1 module fswatch; 2 3 4 debug (FSWTestRun2) version = FSWForcePoll; 5 6 /// 7 enum FileChangeEventType : ubyte 8 { 9 /// Occurs when a file or folder is created. 10 create, 11 /// Occurs when a file or folder is modified. 12 modify, 13 /// Occurs when a file or folder is removed. 14 remove, 15 /// Occurs when a file or folder inside a folder is renamed. 16 rename, 17 /// Occurs when the watched path gets created. 18 createSelf, 19 /// Occurs when the watched path gets deleted. 20 removeSelf 21 } 22 23 /// Structure containing information about filesystem events. 24 struct FileChangeEvent 25 { 26 /// The type of this event. 27 FileChangeEventType type; 28 /// The path of the file of this event. Might not be set for createSelf and removeSelf. 29 string path; 30 /// The path the file got renamed to for a rename event. 31 string newPath = null; 32 } 33 34 35 36 version (FSWForcePoll) 37 version = FSWUsesPolling; 38 else 39 { 40 version (Windows) 41 version = FSWUsesWin32; 42 else version (linux) 43 version = FSWUsesINotify; 44 else version = FSWUsesPolling; 45 } 46 47 /// An instance of a FileWatcher 48 /// Contains different implementations (win32 api, inotify and polling using the std.file methods) 49 /// Specify `version = FSWForcePoll;` to force using std.file (is slower and more resource intensive than the other implementations) 50 struct FileWatch 51 { 52 // internal path variable which shouldn't be changed because it will not update inotify/poll/win32 structures uniformly. 53 string _path; 54 55 /// Path of the file set using the constructor 56 const ref const(string) path() return @property @safe @nogc nothrow pure 57 { 58 return _path; 59 } 60 61 version (FSWUsesWin32) 62 { 63 64 /* 65 * The Windows version works by first creating an asynchronous path handle using CreateFile. 66 * The name may suggest this creates a new file on disk, but it actually gives 67 * a handle to basically anything I/O related. By using the flags FILE_FLAG_OVERLAPPED 68 * and FILE_FLAG_BACKUP_SEMANTICS it can be used in ReadDirectoryChangesW. 69 * 'Overlapped' here means asynchronous, it can also be done synchronously but that would 70 * mean getEvents() would wait until a directory change is registered. 71 * The asynchronous results can be received in a callback, but since FSWatch is polling 72 * based it polls the results using GetOverlappedResult. If messages are received, 73 * ReadDirectoryChangesW is called again. 74 * The function will not notify when the watched directory itself is removed, so 75 * if it doesn't exist anymore the handle is closed and set to null until it exists again. 76 */ 77 import core.sys.windows.basetsd : HANDLE; 78 import fswatch_helpers; 79 80 import core.sys.windows.winbase: OPEN_EXISTING, FILE_FLAG_OVERLAPPED, OVERLAPPED, 81 CloseHandle, GetOverlappedResult, CreateFile, GetLastError, ReadDirectoryChangesW, 82 FILE_FLAG_BACKUP_SEMANTICS; 83 84 import core.sys.windows.winnt: FILE_NOTIFY_INFORMATION, FILE_ACTION_ADDED, 85 FILE_ACTION_REMOVED, FILE_ACTION_MODIFIED, 86 FILE_ACTION_RENAMED_NEW_NAME, FILE_ACTION_RENAMED_OLD_NAME, 87 FILE_LIST_DIRECTORY, FILE_SHARE_WRITE, FILE_SHARE_READ, 88 FILE_SHARE_DELETE, FILE_NOTIFY_CHANGE_FILE_NAME, 89 FILE_NOTIFY_CHANGE_DIR_NAME, FILE_NOTIFY_CHANGE_LAST_WRITE, 90 ERROR_IO_PENDING, ERROR_IO_INCOMPLETE, DWORD; 91 import std.utf : toUTF8, toUTF16, toUTF16z; 92 import hip.util.conv : to, toHex; 93 94 private HANDLE pathHandle; // Windows 'file' handle for ReadDirectoryChangesW 95 private ubyte[1024 * 4] changeBuffer; // 4kb buffer for file changes 96 private bool isDir, exists, recursive; 97 private size_t timeLastModified; 98 private DWORD receivedBytes; 99 private OVERLAPPED overlapObj; 100 private bool queued; // Whether a directory changes watch is issued to Windows 101 private string _absolutePath; 102 103 /// Creates an instance using the Win32 API 104 this(string path, bool recursive = false, bool treatDirAsFile = false) 105 { 106 import fswatch_helpers; 107 import hip.util.path; 108 _path = path; 109 _absolutePath = joinPath(winGetCwd, path); 110 this.recursive = recursive; 111 isDir = !treatDirAsFile; 112 if (!isDir && recursive) 113 throw new Exception("Can't recursively check on a file"); 114 getEvents(); // To create a path handle and start the watch queue 115 // The result, likely containing just 'createSelf' or 'removeSelf', is discarded 116 // This way, the first actual call to getEvents() returns actual events 117 } 118 119 ~this() 120 { 121 CloseHandle(pathHandle); 122 } 123 124 private void startWatchQueue() 125 { 126 if (!ReadDirectoryChangesW(pathHandle, changeBuffer.ptr, changeBuffer.length, recursive, 127 FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE, 128 &receivedBytes, &overlapObj, null)) 129 throw new Exception("Failed to start directory watch queue. Error 0x" ~ GetLastError() 130 .toHex); 131 queued = true; 132 } 133 134 /// Implementation using Win32 API or polling for files 135 FileChangeEvent[] getEvents() 136 { 137 const(wchar)* wAbsolutePath = toUTF16z(_absolutePath); 138 const pathExists = winExists(wAbsolutePath); // cached so it is not called twice 139 if (isDir && (!pathExists || winIsDir(wAbsolutePath))) 140 { 141 // ReadDirectoryChangesW does not report changes to the specified directory 142 // itself, so 'removeself' is checked manually 143 if (!pathExists) 144 { 145 if (pathHandle) 146 { 147 if (GetOverlappedResult(pathHandle, &overlapObj, &receivedBytes, false)) 148 { 149 } 150 queued = false; 151 CloseHandle(pathHandle); 152 pathHandle = null; 153 return [FileChangeEvent(FileChangeEventType.removeSelf, ".")]; 154 } 155 return []; 156 } 157 FileChangeEvent[] events; 158 if (!pathHandle) 159 { 160 pathHandle = CreateFile((_absolutePath.toUTF16 ~ cast(wchar) 0).ptr, FILE_LIST_DIRECTORY, 161 FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE, 162 null, OPEN_EXISTING, 163 FILE_FLAG_OVERLAPPED | FILE_FLAG_BACKUP_SEMANTICS, null); 164 if (!pathHandle) 165 throw new Exception("Error opening directory. Error code 0x" ~ GetLastError() 166 .toHex); 167 queued = false; 168 events ~= FileChangeEvent(FileChangeEventType.createSelf, "."); 169 } 170 if (!queued) 171 { 172 startWatchQueue(); 173 } 174 else 175 { 176 // ReadDirectoryW can give double modify messages, making the queue one event behind 177 // This sequence is repeated as a fix for now, until the intricacy of WinAPI is figured out 178 foreach(_; 0..2) 179 { 180 if (GetOverlappedResult(pathHandle, &overlapObj, &receivedBytes, false)) 181 { 182 int i = 0; 183 string fromFilename; 184 while (true) 185 { 186 auto info = cast(FILE_NOTIFY_INFORMATION*)(changeBuffer.ptr + i); 187 string fileName = (cast(wchar[])( 188 cast(ubyte*) info.FileName)[0 .. info.FileNameLength]) 189 .toUTF8.idup; 190 switch (info.Action) 191 { 192 case FILE_ACTION_ADDED: 193 events ~= FileChangeEvent(FileChangeEventType.create, fileName); 194 break; 195 case FILE_ACTION_REMOVED: 196 events ~= FileChangeEvent(FileChangeEventType.remove, fileName); 197 break; 198 case FILE_ACTION_MODIFIED: 199 events ~= FileChangeEvent(FileChangeEventType.modify, fileName); 200 break; 201 case FILE_ACTION_RENAMED_OLD_NAME: 202 fromFilename = fileName; 203 break; 204 case FILE_ACTION_RENAMED_NEW_NAME: 205 events ~= FileChangeEvent(FileChangeEventType.rename, 206 fromFilename, fileName); 207 break; 208 default: 209 throw new Exception("Unknown file notify action 0x" ~ info.Action.toHex); 210 } 211 i += info.NextEntryOffset; 212 if (info.NextEntryOffset == 0) 213 break; 214 } 215 queued = false; 216 startWatchQueue(); 217 } 218 else 219 { 220 if (GetLastError() != ERROR_IO_PENDING 221 && GetLastError() != ERROR_IO_INCOMPLETE) 222 throw new Exception("Error receiving changes. Error code 0x"~ GetLastError().toHex); 223 break; 224 } 225 } 226 } 227 return events; 228 } 229 else 230 { 231 const nowExists = winExists(wAbsolutePath); 232 if (nowExists && !exists) 233 { 234 exists = true; 235 timeLastModified = fswatch_helpers.timeLastModified(wAbsolutePath); 236 return [FileChangeEvent(FileChangeEventType.createSelf, _absolutePath)]; 237 } 238 else if (!nowExists && exists) 239 { 240 exists = false; 241 return [FileChangeEvent(FileChangeEventType.removeSelf, _absolutePath)]; 242 } 243 else if (nowExists) 244 { 245 const modTime = fswatch_helpers.timeLastModified(wAbsolutePath); 246 if (modTime != timeLastModified) 247 { 248 timeLastModified = modTime; 249 return [FileChangeEvent(FileChangeEventType.modify, _absolutePath)]; 250 } 251 else 252 return []; 253 } 254 else 255 return []; 256 } 257 } 258 } 259 else version (FSWUsesINotify) 260 { 261 262 import core.sys.linux.errno : errno; 263 import core.sys.posix.poll : pollfd, poll, POLLIN; 264 265 import core.stdc.errno : ENOENT; 266 267 private int fd; 268 private bool recursive; 269 private ubyte[1024 * 4] eventBuffer; // 4kb buffer for events 270 private pollfd pfd; 271 private struct FDInfo { int wd; bool watched; string path; } 272 private FDInfo[] directoryMap; // map every watch descriptor to a directory 273 274 /// Creates an instance using the linux inotify API 275 this(string path, bool recursive = false, bool ignored = false) 276 { 277 _path = path; 278 this.recursive = recursive; 279 getEvents(); 280 } 281 282 ~this() 283 { 284 import core.sys.linux.unistd : close; 285 import core.sys.linux.sys.inotify: inotify_rm_watch; 286 if (fd) 287 { 288 foreach (ref fdinfo; directoryMap) 289 if (fdinfo.watched) 290 inotify_rm_watch(fd, fdinfo.wd); 291 close(fd); 292 } 293 } 294 295 private void addWatch(string path) 296 { 297 import hip.util.string : toStringz; 298 import hip.util.conv : to; 299 import core.sys.linux.fcntl : fcntl, F_SETFD, FD_CLOEXEC; 300 import core.sys.linux.sys.inotify: inotify_add_watch, IN_CREATE, IN_DELETE, 301 IN_DELETE_SELF, IN_MODIFY, IN_MOVE_SELF, IN_MOVED_FROM, IN_MOVED_TO, 302 IN_ATTRIB, IN_EXCL_UNLINK; 303 304 305 auto wd = inotify_add_watch(fd, path.toStringz, 306 IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY | IN_MOVE_SELF 307 | IN_MOVED_FROM | IN_MOVED_TO | IN_ATTRIB | IN_EXCL_UNLINK); 308 assert(wd != -1, 309 "inotify_add_watch returned invalid watch descriptor. Error code " 310 ~ errno.to!string); 311 assert(fcntl(fd, F_SETFD, FD_CLOEXEC) != -1, 312 "Could not set FD_CLOEXEC bit. Error code " ~ errno.to!string); 313 directoryMap ~= FDInfo(wd, true, path); 314 } 315 316 /// Implementation using inotify 317 FileChangeEvent[] getEvents() 318 { 319 import std.algorithm : countUntil; 320 import std.file; 321 import hip.util.string: toStringz; 322 import hip.util.conv : to; 323 import std.string : stripRight; 324 import core.sys.linux.unistd : read; 325 import core.sys.linux.fcntl : stat, stat_t, S_ISDIR; 326 import core.sys.linux.sys.inotify : inotify_init1, 327 inotify_event, IN_NONBLOCK, IN_MOVED_TO, IN_CREATE, IN_DELETE, IN_ATTRIB, IN_MOVED_FROM, 328 IN_MOVE_SELF, IN_MODIFY, IN_DELETE_SELF, inotify_rm_watch; 329 330 FileChangeEvent[] events; 331 if (!fd && path.exists) 332 { 333 fd = inotify_init1(IN_NONBLOCK); 334 assert(fd != -1, 335 "inotify_init1 returned invalid file descriptor. Error code " 336 ~ errno.to!string); 337 addWatch(path); 338 events ~= FileChangeEvent(FileChangeEventType.createSelf, path); 339 340 if (recursive) 341 foreach(string subPath; dirEntries(path, SpanMode.depth)) 342 { 343 addWatch(subPath); 344 events ~= FileChangeEvent(FileChangeEventType.createSelf, subPath); 345 } 346 } 347 if (!fd) 348 return events; 349 pfd.fd = fd; 350 pfd.events = POLLIN; 351 const code = poll(&pfd, 1, 0); 352 if (code < 0) 353 throw new Exception("Failed to poll events. Error code " ~ errno.to!string); 354 else if (code == 0) 355 return events; 356 else 357 { 358 import hip.util.path:relativePath, joinPath; 359 const receivedBytes = read(fd, eventBuffer.ptr, eventBuffer.length); 360 int i = 0; 361 string fromFilename; 362 uint cookie; 363 while (true) 364 { 365 auto info = cast(inotify_event*)(eventBuffer.ptr + i); 366 // string fileName = info.name.ptr[0..info.len].idup; 367 string fileName = info.name.ptr[0..info.len].stripRight("\0").idup; 368 auto mapIndex = directoryMap.countUntil!(a => a.wd == info.wd); 369 string absoluteFileName = joinPath('/', directoryMap[mapIndex].path, fileName); 370 string relativeFilename = relativePath("/" ~ absoluteFileName, "/" ~ path); 371 if (cookie && (info.mask & IN_MOVED_TO) == 0) 372 { 373 events ~= FileChangeEvent(FileChangeEventType.remove, fromFilename); 374 fromFilename.length = 0; 375 cookie = 0; 376 } 377 if ((info.mask & IN_CREATE) != 0) 378 { 379 // If a dir/file is created and deleted immediately then 380 // isDir will throw FileException(ENOENT) 381 if (recursive) 382 { 383 stat_t dirCheck; 384 if (stat(absoluteFileName.toStringz, &dirCheck) == 0) 385 { 386 if (S_ISDIR(dirCheck.st_mode)) 387 addWatch(absoluteFileName); 388 } 389 else 390 { 391 const err = errno; 392 if (err != ENOENT) 393 throw new FileException(absoluteFileName, err); 394 } 395 } 396 397 events ~= FileChangeEvent(FileChangeEventType.create, relativeFilename); 398 } 399 if ((info.mask & IN_DELETE) != 0) 400 events ~= FileChangeEvent(FileChangeEventType.remove, relativeFilename); 401 if ((info.mask & IN_MODIFY) != 0 || (info.mask & IN_ATTRIB) != 0) 402 events ~= FileChangeEvent(FileChangeEventType.modify, relativeFilename); 403 if ((info.mask & IN_MOVED_FROM) != 0) 404 { 405 fromFilename = fileName; 406 cookie = info.cookie; 407 } 408 if ((info.mask & IN_MOVED_TO) != 0) 409 { 410 if (info.cookie == cookie) 411 { 412 events ~= FileChangeEvent(FileChangeEventType.rename, 413 fromFilename, relativeFilename); 414 } 415 else 416 events ~= FileChangeEvent(FileChangeEventType.create, relativeFilename); 417 cookie = 0; 418 } 419 if ((info.mask & IN_DELETE_SELF) != 0 || (info.mask & IN_MOVE_SELF) != 0) 420 { 421 if (fd) 422 { 423 inotify_rm_watch(fd, info.wd); 424 directoryMap[mapIndex].watched = false; 425 } 426 if (directoryMap[mapIndex].path == path) 427 events ~= FileChangeEvent(FileChangeEventType.removeSelf, "."); 428 } 429 i += inotify_event.sizeof + info.len; 430 if (i >= receivedBytes) 431 break; 432 } 433 if (cookie) 434 { 435 events ~= FileChangeEvent(FileChangeEventType.remove, fromFilename); 436 fromFilename.length = 0; 437 cookie = 0; 438 } 439 } 440 return events; 441 } 442 } 443 else version (FSWUsesPolling) 444 { 445 import std.datetime : SysTime; 446 import std.algorithm : countUntil, remove; 447 import std.path : relativePath, absolutePath, baseName; 448 private ulong getUniqueHash(DirEntry entry) 449 { 450 version (Windows) 451 return entry.timeCreated.stdTime ^ cast(ulong) entry.attributes; 452 else version (Posix) 453 return entry.statBuf.st_ino | (cast(ulong) entry.statBuf.st_dev << 32UL); 454 else 455 return (entry.timeLastModified.stdTime ^ ( 456 cast(ulong) entry.attributes << 32UL) ^ entry.linkAttributes) * entry.size; 457 } 458 459 private struct FileEntryCache 460 { 461 SysTime lastModification; 462 const string name; 463 bool isDirty; 464 ulong uniqueHash; 465 } 466 467 private FileEntryCache[] cache; 468 private bool isDir, recursive, exists; 469 private SysTime timeLastModified; 470 private string cwd; 471 472 /// Generic fallback implementation using std.file.dirEntries 473 this(string path, bool recursive = false, bool treatDirAsFile = false) 474 { 475 _path = path; 476 cwd = getcwd; 477 this.recursive = recursive; 478 isDir = !treatDirAsFile; 479 if (!isDir && recursive) 480 throw new Exception("Can't recursively check on a file"); 481 getEvents(); 482 } 483 484 /// Generic polling implementation 485 FileChangeEvent[] getEvents() 486 { 487 const nowExists = path.exists; 488 if (isDir && (!nowExists || path.isDir)) 489 { 490 FileChangeEvent[] events; 491 if (nowExists && !exists) 492 { 493 exists = true; 494 events ~= FileChangeEvent(FileChangeEventType.createSelf, "."); 495 } 496 if (!nowExists && exists) 497 { 498 exists = false; 499 return [FileChangeEvent(FileChangeEventType.removeSelf, ".")]; 500 } 501 if (!nowExists) 502 return []; 503 foreach (ref e; cache) 504 e.isDirty = true; 505 DirEntry[] created; 506 foreach (file; dirEntries(path, recursive ? SpanMode.depth : SpanMode.shallow)) 507 { 508 auto newCache = FileEntryCache(file.timeLastModified, 509 file.name, false, file.getUniqueHash); 510 bool found = false; 511 foreach (ref cacheEntry; cache) 512 { 513 if (cacheEntry.name == newCache.name) 514 { 515 if (cacheEntry.lastModification != newCache.lastModification) 516 { 517 cacheEntry.lastModification = newCache.lastModification; 518 events ~= FileChangeEvent(FileChangeEventType.modify, 519 relativePath(file.name.absolutePath(cwd), 520 path.absolutePath(cwd))); 521 } 522 cacheEntry.isDirty = false; 523 found = true; 524 break; 525 } 526 } 527 if (!found) 528 { 529 cache ~= newCache; 530 created ~= file; 531 } 532 } 533 foreach_reverse (i, ref e; cache) 534 { 535 if (e.isDirty) 536 { 537 auto idx = created.countUntil!((a, b) => a.getUniqueHash == b.uniqueHash)(e); 538 if (idx != -1) 539 { 540 events ~= FileChangeEvent(FileChangeEventType.rename, 541 relativePath(e.name.absolutePath(cwd), 542 path.absolutePath(cwd)), relativePath(created[idx].name.absolutePath(cwd), 543 path.absolutePath(cwd))); 544 created = created.remove(idx); 545 } 546 else 547 { 548 events ~= FileChangeEvent(FileChangeEventType.remove, 549 relativePath(e.name.absolutePath(cwd), path.absolutePath(cwd))); 550 } 551 cache = cache.remove(i); 552 } 553 } 554 foreach (ref e; created) 555 { 556 events ~= FileChangeEvent(FileChangeEventType.create, 557 relativePath(e.name.absolutePath(cwd), path.absolutePath(cwd))); 558 } 559 if (events.length && events[0].type == FileChangeEventType.createSelf) 560 return [events[0]]; 561 return events; 562 } 563 else 564 { 565 if (nowExists && !exists) 566 { 567 exists = true; 568 timeLastModified = path.timeLastModified; 569 return [FileChangeEvent(FileChangeEventType.createSelf, ".")]; 570 } 571 else if (!nowExists && exists) 572 { 573 exists = false; 574 return [FileChangeEvent(FileChangeEventType.removeSelf, ".")]; 575 } 576 else if (nowExists) 577 { 578 const modTime = path.timeLastModified; 579 if (modTime != timeLastModified) 580 { 581 timeLastModified = modTime; 582 return [FileChangeEvent(FileChangeEventType.modify, path.baseName)]; 583 } 584 else 585 return []; 586 } 587 else 588 return []; 589 } 590 } 591 } 592 else 593 static assert(0, "No filesystem watching method?! Try setting version = FSWForcePoll;"); 594 } 595 596 /// 597 unittest 598 { 599 import core.thread; 600 601 FileChangeEvent waitForEvent(ref FileWatch watcher) 602 { 603 FileChangeEvent[] ret; 604 while ((ret = watcher.getEvents()).length == 0) 605 { 606 Thread.sleep(1.msecs); 607 } 608 return ret[0]; 609 } 610 611 if (exists("test")) 612 rmdirRecurse("test"); 613 scope (exit) 614 { 615 if (exists("test")) 616 rmdirRecurse("test"); 617 } 618 619 auto watcher = FileWatch("test", true); 620 assert(watcher.path == "test"); 621 mkdir("test"); 622 auto ev = waitForEvent(watcher); 623 assert(ev.type == FileChangeEventType.createSelf); 624 write("test/a.txt", "abc"); 625 ev = waitForEvent(watcher); 626 assert(ev.type == FileChangeEventType.create); 627 assert(ev.path == "a.txt"); 628 Thread.sleep(2000.msecs); // for polling variant 629 append("test/a.txt", "def"); 630 ev = waitForEvent(watcher); 631 assert(ev.type == FileChangeEventType.modify); 632 assert(ev.path == "a.txt"); 633 rename("test/a.txt", "test/b.txt"); 634 ev = waitForEvent(watcher); 635 assert(ev.type == FileChangeEventType.rename); 636 assert(ev.path == "a.txt"); 637 assert(ev.newPath == "b.txt"); 638 remove("test/b.txt"); 639 ev = waitForEvent(watcher); 640 assert(ev.type == FileChangeEventType.remove); 641 assert(ev.path == "b.txt"); 642 rmdirRecurse("test"); 643 ev = waitForEvent(watcher); 644 assert(ev.type == FileChangeEventType.removeSelf); 645 } 646 647 version (linux) unittest 648 { 649 import core.thread; 650 651 FileChangeEvent waitForEvent(ref FileWatch watcher, Duration timeout = 2.seconds) 652 { 653 FileChangeEvent[] ret; 654 Duration elapsed; 655 while ((ret = watcher.getEvents()).length == 0) 656 { 657 Thread.sleep(1.msecs); 658 elapsed += 1.msecs; 659 if (elapsed >= timeout) 660 throw new Exception("timeout"); 661 } 662 return ret[0]; 663 } 664 665 if (exists("test2")) 666 rmdirRecurse("test2"); 667 if (exists("test3")) 668 rmdirRecurse("test3"); 669 scope (exit) 670 { 671 if (exists("test2")) 672 rmdirRecurse("test2"); 673 if (exists("test3")) 674 rmdirRecurse("test3"); 675 } 676 677 auto watcher = FileWatch("test2", true); 678 mkdir("test2"); 679 auto ev = waitForEvent(watcher); 680 assert(ev.type == FileChangeEventType.createSelf); 681 write("test2/a.txt", "abc"); 682 ev = waitForEvent(watcher); 683 assert(ev.type == FileChangeEventType.create); 684 assert(ev.path == "a.txt"); 685 rename("test2/a.txt", "./testfile-a.txt"); 686 ev = waitForEvent(watcher); 687 assert(ev.type == FileChangeEventType.remove); 688 assert(ev.path == "a.txt"); 689 rename("./testfile-a.txt", "test2/b.txt"); 690 ev = waitForEvent(watcher); 691 assert(ev.type == FileChangeEventType.create); 692 assert(ev.path == "b.txt"); 693 remove("test2/b.txt"); 694 ev = waitForEvent(watcher); 695 assert(ev.type == FileChangeEventType.remove); 696 assert(ev.path == "b.txt"); 697 698 mkdir("test2/mydir"); 699 rmdir("test2/mydir"); 700 try 701 { 702 ev = waitForEvent(watcher); 703 // waitForEvent only returns first event (just a test method anyway) because on windows or unprecise platforms events can be spawned multiple times 704 // or could be never fired in case of slow polling mechanism 705 assert(ev.type == FileChangeEventType.create); 706 assert(ev.path == "mydir"); 707 } 708 catch (Exception e) 709 { 710 if (e.msg != "timeout") 711 throw e; 712 } 713 714 version (FSWUsesINotify) 715 { 716 // test for creation, modification, removal of subdirectory 717 mkdir("test2/subdir"); 718 ev = waitForEvent(watcher); 719 assert(ev.type == FileChangeEventType.create); 720 assert(ev.path == "subdir"); 721 write("test2/subdir/c.txt", "abc"); 722 ev = waitForEvent(watcher); 723 assert(ev.type == FileChangeEventType.create); 724 assert(ev.path == "subdir/c.txt"); 725 write("test2/subdir/c.txt", "\nabc"); 726 ev = waitForEvent(watcher); 727 assert(ev.type == FileChangeEventType.modify); 728 assert(ev.path == "subdir/c.txt"); 729 rmdirRecurse("test2/subdir"); 730 auto events = watcher.getEvents(); 731 assert(events[0].type == FileChangeEventType.remove); 732 assert(events[0].path == "subdir/c.txt"); 733 assert(events[1].type == FileChangeEventType.remove); 734 assert(events[1].path == "subdir"); 735 } 736 // removal of watched folder 737 rmdirRecurse("test2"); 738 ev = waitForEvent(watcher); 739 assert(ev.type == FileChangeEventType.removeSelf); 740 assert(ev.path == "."); 741 742 version (FSWUsesINotify) 743 { 744 // test for a subdirectory already present 745 // both when recursive = true and recursive = false 746 foreach (recursive; [true, false]) 747 { 748 mkdir("test3"); 749 mkdir("test3/a"); 750 mkdir("test3/a/b"); 751 watcher = FileWatch("test3", recursive); 752 write("test3/a/b/c.txt", "abc"); 753 if (recursive) 754 { 755 ev = waitForEvent(watcher); 756 assert(ev.type == FileChangeEventType.create); 757 assert(ev.path == "a/b/c.txt"); 758 } 759 if (!recursive) 760 { 761 // creation of subdirectory and file within 762 // test that addWatch doesn't get called 763 mkdir("test3/d"); 764 write("test3/d/e.txt", "abc"); 765 auto revents = watcher.getEvents(); 766 assert(revents.length == 1); 767 assert(revents[0].type == FileChangeEventType.create); 768 assert(revents[0].path == "d"); 769 rmdirRecurse("test3/d"); 770 revents = watcher.getEvents(); 771 assert(revents.length == 1); 772 assert(revents[0].type == FileChangeEventType.remove); 773 assert(revents[0].path == "d"); 774 } 775 rmdirRecurse("test3"); 776 events = watcher.getEvents(); 777 if (recursive) 778 { 779 assert(events.length == 4); 780 assert(events[0].type == FileChangeEventType.remove); 781 assert(events[0].path == "a/b/c.txt"); 782 assert(events[1].type == FileChangeEventType.remove); 783 assert(events[1].path == "a/b"); 784 assert(events[2].type == FileChangeEventType.remove); 785 assert(events[2].path == "a"); 786 assert(events[3].type == FileChangeEventType.removeSelf); 787 assert(events[3].path == "."); 788 } 789 else 790 { 791 assert(events.length == 2); 792 assert(events[0].type == FileChangeEventType.remove); 793 assert(events[0].path == "a"); 794 assert(events[1].type == FileChangeEventType.removeSelf); 795 assert(events[1].path == "."); 796 } 797 } 798 } 799 }